<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Cache
 * @subpackage Zend_Cache_Backend
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Memcached.php 23775 2011-03-01 17:25:24Z ralph $
 */

/**
 * @see Zend_Cache_Backend_Interface
 */
require_once 'Zend/Cache/Backend/ExtendedInterface.php';

/**
 * @see Zend_Cache_Backend
 */
require_once 'Zend/Cache/Backend.php';

/**
 * @package    Zend_Cache
 * @subpackage Zend_Cache_Backend
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Cache_Backend_Memcached extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface {
	/**
	 * Default Values
	 */
	const DEFAULT_HOST = '127.0.0.1';
	const DEFAULT_PORT = 11211;
	const DEFAULT_PERSISTENT = true;
	const DEFAULT_WEIGHT = 1;
	const DEFAULT_TIMEOUT = 1;
	const DEFAULT_RETRY_INTERVAL = 15;
	const DEFAULT_STATUS = true;
	const DEFAULT_FAILURE_CALLBACK = null;
	
	/**
	 * Log message
	 */
	const TAGS_UNSUPPORTED_BY_CLEAN_OF_MEMCACHED_BACKEND = 'Zend_Cache_Backend_Memcached::clean() : tags are unsupported by the Memcached backend';
	const TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND = 'Zend_Cache_Backend_Memcached::save() : tags are unsupported by the Memcached backend';
	
	/**
	 * Available options
	 *
	 * =====> (array) servers :
	 * an array of memcached server ; each memcached server is described by an associative array :
	 * 'host' => (string) : the name of the memcached server
	 * 'port' => (int) : the port of the memcached server
	 * 'persistent' => (bool) : use or not persistent connections to this memcached server
	 * 'weight' => (int) : number of buckets to create for this server which in turn control its
	 * probability of it being selected. The probability is relative to the total
	 * weight of all servers.
	 * 'timeout' => (int) : value in seconds which will be used for connecting to the daemon. Think twice
	 * before changing the default value of 1 second - you can lose all the
	 * advantages of caching if your connection is too slow.
	 * 'retry_interval' => (int) : controls how often a failed server will be retried, the default value
	 * is 15 seconds. Setting this parameter to -1 disables automatic retry.
	 * 'status' => (bool) : controls if the server should be flagged as online.
	 * 'failure_callback' => (callback) : Allows the user to specify a callback function to run upon
	 * encountering an error. The callback is run before failover
	 * is attempted. The function takes two parameters, the hostname
	 * and port of the failed server.
	 *
	 * =====> (boolean) compression :
	 * true if you want to use on-the-fly compression
	 *
	 * =====> (boolean) compatibility :
	 * true if you use old memcache server or extension
	 *
	 * @var array available options
	 */
	protected $_options = array ('servers' => array (array ('host' => self::DEFAULT_HOST, 'port' => self::DEFAULT_PORT, 'persistent' => self::DEFAULT_PERSISTENT, 'weight' => self::DEFAULT_WEIGHT, 'timeout' => self::DEFAULT_TIMEOUT, 'retry_interval' => self::DEFAULT_RETRY_INTERVAL, 'status' => self::DEFAULT_STATUS, 'failure_callback' => self::DEFAULT_FAILURE_CALLBACK ) ), 'compression' => false, 'compatibility' => false );
	
	/**
	 * Memcache object
	 *
	 * @var mixed memcache object
	 */
	protected $_memcache = null;
	
	/**
	 * Constructor
	 *
	 * @param array $options associative array of options
	 * @throws Zend_Cache_Exception
	 * @return void
	 */
	public function __construct(array $options = array()) {
		if (! extension_loaded ( 'memcache' )) {
			Zend_Cache::throwException ( 'The memcache extension must be loaded for using this backend !' );
		}
		parent::__construct ( $options );
		if (isset ( $this->_options ['servers'] )) {
			$value = $this->_options ['servers'];
			if (isset ( $value ['host'] )) {
				// in this case, $value seems to be a simple associative array (one server only)
				$value = array (0 => $value ); // let's transform it into a classical array of associative arrays
			}
			$this->setOption ( 'servers', $value );
		}
		$this->_memcache = new Memcache ();
		foreach ( $this->_options ['servers'] as $server ) {
			if (! array_key_exists ( 'port', $server )) {
				$server ['port'] = self::DEFAULT_PORT;
			}
			if (! array_key_exists ( 'persistent', $server )) {
				$server ['persistent'] = self::DEFAULT_PERSISTENT;
			}
			if (! array_key_exists ( 'weight', $server )) {
				$server ['weight'] = self::DEFAULT_WEIGHT;
			}
			if (! array_key_exists ( 'timeout', $server )) {
				$server ['timeout'] = self::DEFAULT_TIMEOUT;
			}
			if (! array_key_exists ( 'retry_interval', $server )) {
				$server ['retry_interval'] = self::DEFAULT_RETRY_INTERVAL;
			}
			if (! array_key_exists ( 'status', $server )) {
				$server ['status'] = self::DEFAULT_STATUS;
			}
			if (! array_key_exists ( 'failure_callback', $server )) {
				$server ['failure_callback'] = self::DEFAULT_FAILURE_CALLBACK;
			}
			if ($this->_options ['compatibility']) {
				// No status for compatibility mode (#ZF-5887)
				$this->_memcache->addServer ( $server ['host'], $server ['port'], $server ['persistent'], $server ['weight'], $server ['timeout'], $server ['retry_interval'] );
			} else {
				$this->_memcache->addServer ( $server ['host'], $server ['port'], $server ['persistent'], $server ['weight'], $server ['timeout'], $server ['retry_interval'], $server ['status'], $server ['failure_callback'] );
			}
		}
	}
	
	/**
	 * Test if a cache is available for the given id and (if yes) return it (false else)
	 *
	 * @param  string  $id                     Cache id
	 * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
	 * @return string|false cached datas
	 */
	public function load($id, $doNotTestCacheValidity = false) {
		$tmp = $this->_memcache->get ( $id );
		if (is_array ( $tmp ) && isset ( $tmp [0] )) {
			return $tmp [0];
		}
		return false;
	}
	
	/**
	 * Test if a cache is available or not (for the given id)
	 *
	 * @param  string $id Cache id
	 * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
	 */
	public function test($id) {
		$tmp = $this->_memcache->get ( $id );
		if (is_array ( $tmp )) {
			return $tmp [1];
		}
		return false;
	}
	
	/**
	 * Save some string datas into a cache record
	 *
	 * Note : $data is always "string" (serialization is done by the
	 * core not by the backend)
	 *
	 * @param  string $data             Datas to cache
	 * @param  string $id               Cache id
	 * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry
	 * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
	 * @return boolean True if no problem
	 */
	public function save($data, $id, $tags = array(), $specificLifetime = false) {
		$lifetime = $this->getLifetime ( $specificLifetime );
		if ($this->_options ['compression']) {
			$flag = MEMCACHE_COMPRESSED;
		} else {
			$flag = 0;
		}
		
		// ZF-8856: using set because add needs a second request if item already exists
		$result = @$this->_memcache->set ( $id, array ($data, time (), $lifetime ), $flag, $lifetime );
		
		if (count ( $tags ) > 0) {
			$this->_log ( self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND );
		}
		
		return $result;
	}
	
	/**
	 * Remove a cache record
	 *
	 * @param  string $id Cache id
	 * @return boolean True if no problem
	 */
	public function remove($id) {
		return $this->_memcache->delete ( $id, 0 );
	}
	
	/**
	 * Clean some cache records
	 *
	 * Available modes are :
	 * 'all' (default)  => remove all cache entries ($tags is not used)
	 * 'old'            => unsupported
	 * 'matchingTag'    => unsupported
	 * 'notMatchingTag' => unsupported
	 * 'matchingAnyTag' => unsupported
	 *
	 * @param  string $mode Clean mode
	 * @param  array  $tags Array of tags
	 * @throws Zend_Cache_Exception
	 * @return boolean True if no problem
	 */
	public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {
		switch ($mode) {
			case Zend_Cache::CLEANING_MODE_ALL :
				return $this->_memcache->flush ();
				break;
			case Zend_Cache::CLEANING_MODE_OLD :
				$this->_log ( "Zend_Cache_Backend_Memcached::clean() : CLEANING_MODE_OLD is unsupported by the Memcached backend" );
				break;
			case Zend_Cache::CLEANING_MODE_MATCHING_TAG :
			case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG :
			case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG :
				$this->_log ( self::TAGS_UNSUPPORTED_BY_CLEAN_OF_MEMCACHED_BACKEND );
				break;
			default :
				Zend_Cache::throwException ( 'Invalid mode for clean() method' );
				break;
		}
	}
	
	/**
	 * Return true if the automatic cleaning is available for the backend
	 *
	 * @return boolean
	 */
	public function isAutomaticCleaningAvailable() {
		return false;
	}
	
	/**
	 * Set the frontend directives
	 *
	 * @param  array $directives Assoc of directives
	 * @throws Zend_Cache_Exception
	 * @return void
	 */
	public function setDirectives($directives) {
		parent::setDirectives ( $directives );
		$lifetime = $this->getLifetime ( false );
		if ($lifetime > 2592000) {
			// #ZF-3490 : For the memcached backend, there is a lifetime limit of 30 days (2592000 seconds)
			$this->_log ( 'memcached backend has a limit of 30 days (2592000 seconds) for the lifetime' );
		}
		if ($lifetime === null) {
			// #ZF-4614 : we tranform null to zero to get the maximal lifetime
			parent::setDirectives ( array ('lifetime' => 0 ) );
		}
	}
	
	/**
	 * Return an array of stored cache ids
	 *
	 * @return array array of stored cache ids (string)
	 */
	public function getIds() {
		$this->_log ( "Zend_Cache_Backend_Memcached::save() : getting the list of cache ids is unsupported by the Memcache backend" );
		return array ();
	}
	
	/**
	 * Return an array of stored tags
	 *
	 * @return array array of stored tags (string)
	 */
	public function getTags() {
		$this->_log ( self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND );
		return array ();
	}
	
	/**
	 * Return an array of stored cache ids which match given tags
	 *
	 * In case of multiple tags, a logical AND is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of matching cache ids (string)
	 */
	public function getIdsMatchingTags($tags = array()) {
		$this->_log ( self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND );
		return array ();
	}
	
	/**
	 * Return an array of stored cache ids which don't match given tags
	 *
	 * In case of multiple tags, a logical OR is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of not matching cache ids (string)
	 */
	public function getIdsNotMatchingTags($tags = array()) {
		$this->_log ( self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND );
		return array ();
	}
	
	/**
	 * Return an array of stored cache ids which match any given tags
	 *
	 * In case of multiple tags, a logical AND is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of any matching cache ids (string)
	 */
	public function getIdsMatchingAnyTags($tags = array()) {
		$this->_log ( self::TAGS_UNSUPPORTED_BY_SAVE_OF_MEMCACHED_BACKEND );
		return array ();
	}
	
	/**
	 * Return the filling percentage of the backend storage
	 *
	 * @throws Zend_Cache_Exception
	 * @return int integer between 0 and 100
	 */
	public function getFillingPercentage() {
		$mems = $this->_memcache->getExtendedStats ();
		
		$memSize = null;
		$memUsed = null;
		foreach ( $mems as $key => $mem ) {
			if ($mem === false) {
				$this->_log ( 'can\'t get stat from ' . $key );
				continue;
			}
			
			$eachSize = $mem ['limit_maxbytes'];
			$eachUsed = $mem ['bytes'];
			if ($eachUsed > $eachSize) {
				$eachUsed = $eachSize;
			}
			
			$memSize += $eachSize;
			$memUsed += $eachUsed;
		}
		
		if ($memSize === null || $memUsed === null) {
			Zend_Cache::throwException ( 'Can\'t get filling percentage' );
		}
		
		return (( int ) (100. * ($memUsed / $memSize)));
	}
	
	/**
	 * Return an array of metadatas for the given cache id
	 *
	 * The array must include these keys :
	 * - expire : the expire timestamp
	 * - tags : a string array of tags
	 * - mtime : timestamp of last modification time
	 *
	 * @param string $id cache id
	 * @return array array of metadatas (false if the cache id is not found)
	 */
	public function getMetadatas($id) {
		$tmp = $this->_memcache->get ( $id );
		if (is_array ( $tmp )) {
			$data = $tmp [0];
			$mtime = $tmp [1];
			if (! isset ( $tmp [2] )) {
				// because this record is only with 1.7 release
				// if old cache records are still there...
				return false;
			}
			$lifetime = $tmp [2];
			return array ('expire' => $mtime + $lifetime, 'tags' => array (), 'mtime' => $mtime );
		}
		return false;
	}
	
	/**
	 * Give (if possible) an extra lifetime to the given cache id
	 *
	 * @param string $id cache id
	 * @param int $extraLifetime
	 * @return boolean true if ok
	 */
	public function touch($id, $extraLifetime) {
		if ($this->_options ['compression']) {
			$flag = MEMCACHE_COMPRESSED;
		} else {
			$flag = 0;
		}
		$tmp = $this->_memcache->get ( $id );
		if (is_array ( $tmp )) {
			$data = $tmp [0];
			$mtime = $tmp [1];
			if (! isset ( $tmp [2] )) {
				// because this record is only with 1.7 release
				// if old cache records are still there...
				return false;
			}
			$lifetime = $tmp [2];
			$newLifetime = $lifetime - (time () - $mtime) + $extraLifetime;
			if ($newLifetime <= 0) {
				return false;
			}
			// #ZF-5702 : we try replace() first becase set() seems to be slower
			if (! ($result = $this->_memcache->replace ( $id, array ($data, time (), $newLifetime ), $flag, $newLifetime ))) {
				$result = $this->_memcache->set ( $id, array ($data, time (), $newLifetime ), $flag, $newLifetime );
			}
			return $result;
		}
		return false;
	}
	
	/**
	 * Return an associative array of capabilities (booleans) of the backend
	 *
	 * The array must include these keys :
	 * - automatic_cleaning (is automating cleaning necessary)
	 * - tags (are tags supported)
	 * - expired_read (is it possible to read expired cache records
	 * (for doNotTestCacheValidity option for example))
	 * - priority does the backend deal with priority when saving
	 * - infinite_lifetime (is infinite lifetime can work with this backend)
	 * - get_list (is it possible to get the list of cache ids and the complete list of tags)
	 *
	 * @return array associative of with capabilities
	 */
	public function getCapabilities() {
		return array ('automatic_cleaning' => false, 'tags' => false, 'expired_read' => false, 'priority' => false, 'infinite_lifetime' => false, 'get_list' => false );
	}

}
